Contribute to splice as acceptor#4416
Conversation
|
👋 Thanks for assigning @wpaulino as a reviewer! |
fc3e1da to
758ab0a
Compare
b6cec12 to
5a26025
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4416 +/- ##
==========================================
+ Coverage 85.94% 86.03% +0.09%
==========================================
Files 159 159
Lines 104607 104731 +124
Branches 104607 104731 +124
==========================================
+ Hits 89901 90110 +209
+ Misses 12194 12120 -74
+ Partials 2512 2501 -11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
5a26025 to
658f332
Compare
Now that the Splice variant (containing non-serializable FundingContribution) is the only variant produced, and the previous commit consumes the acceptor's quiescent_action in splice_init(), there is no longer a need to persist it. This allows removing LegacySplice, SpliceInstructions, ChangeStrategy, and related code paths including calculate_change_output, calculate_change_output_value, and the legacy send_splice_init method. With ChangeStrategy removed, the only remaining path in calculate_change_output was FromCoinSelection which always returned Ok(None), making it dead code. The into_interactive_tx_constructor method is simplified accordingly, and the signer_provider parameter is removed from it and from splice_init/splice_ack since it was only needed for the removed change output calculation. On deserialization, quiescent_action (TLV 65) is still read for backwards compatibility but discarded, and the awaiting_quiescence channel state flag is cleared since it cannot be acted upon without a quiescent_action. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the single public InteractiveTxConstructor::new() with separate new_for_outbound() and new_for_inbound() constructors. This moves the initiator's first message preparation out of the core constructor, making it infallible and removing is_initiator from the args struct. Callers no longer need to handle constructor errors, which avoids having to generate SpliceFailed/DiscardFunding events after the QuiescentAction has already been consumed during splice_init/splice_ack handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When constructing a FundingContribution, it's always assumed the estimated_fee is for when used as the initiator, who pays for the common fields and shared inputs / outputs. However, when the contribution is used as the acceptor, we'd be overpaying fees. Provide a method on FundingContribution that adjusts the fees and the change output, if possible.
Add a `change_output: Option<&TxOut>` parameter to `estimate_transaction_fee` so the initial fee estimate accounts for the change output's weight. Previously, the change output weight was omitted from `estimated_fee` in `FundingContribution`, causing the estimate to be slightly too low when a change output was present. This also eliminates an unnecessary `Vec<TxOut>` allocation in `compute_feerate_adjustment`, which previously cloned outputs into a temporary Vec just to include the change output for the fee estimate. A mock `TightBudgetWallet` is added to `splicing_tests` to demonstrate that `validate()` correctly rejects contributions where the input value is sufficient without the change output weight but insufficient with it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
658f332 to
4274f75
Compare
4274f75 to
a1fe138
Compare
When both nodes want to splice simultaneously, the quiescence tie-breaker designates one as the initiator. Previously, the losing node responded with zero contribution, requiring a second full splice session after the first splice locked. This is wasteful, especially for often-offline nodes that may connect and immediately want to splice. Instead, the losing node contributes to the winner's splice as the acceptor, merging both contributions into a single splice transaction. Since the FundingContribution was originally built with initiator fees (which include common fields and shared input/output weight), the fee is adjusted to the acceptor rate before contributing, with the surplus returned to the change output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a1fe138 to
e9c5791
Compare
| {1, LegacySplice} => (), | ||
| ); | ||
| #[cfg(not(any(test, fuzzing, feature = "_test_utils")))] | ||
| impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, |
There was a problem hiding this comment.
Shouldn't this be removed?
| self.send_splice_init_internal(context, ChangeStrategy::LegacyUserProvided(change_script)) | ||
| } | ||
|
|
||
| fn send_splice_init_internal( |
There was a problem hiding this comment.
We can drop _internal now?
| )) | ||
| })?; | ||
| ); | ||
| let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); |
There was a problem hiding this comment.
Let's return this as part of the constructor now and drop take_initiator_first_message?
| /// Returns `Ok((new_estimated_fee, new_change_value))` or `Err`: | ||
| /// - `(fee, Some(change))` — inputs with change: both should be updated | ||
| /// - `(fee, None)` — inputs without change (or change removed), or splice-out: fee updated | ||
| /// only |
| let msg_events = acceptor.node.get_and_clear_pending_msg_events(); | ||
| assert_eq!(msg_events.len(), 1, "{msg_events:?}"); | ||
| if let MessageSendEvent::SendTxComplete { ref msg, .. } = msg_events.remove(0) { | ||
| initiator.node.handle_tx_complete(node_id_acceptor, msg); | ||
| } else { | ||
| panic!(); | ||
| match &msg_events[0] { |
There was a problem hiding this comment.
Let's make the other node follow this pattern as well?
| .map_err(|e| { | ||
| log_info!( | ||
| logger, | ||
| "Cannot accommodate initiator's feerate for channel {}: {}; \ |
There was a problem hiding this comment.
Include the feerate we couldn't afford
| let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; | ||
| let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64); | ||
| let our_funding_contribution = self.queued_funding_contribution().and_then(|c| { | ||
| c.net_value_for_acceptor_at_feerate(feerate) |
There was a problem hiding this comment.
We probably shouldn't blindly accept the counterparty's feerate, but asking the user to approve it isn't ideal either.
| /// - `(fee, Some(change))` — inputs with change: both should be updated | ||
| /// - `(fee, None)` — inputs without change (or change removed), or splice-out: fee updated | ||
| /// only | ||
| fn compute_feerate_adjustment( |
There was a problem hiding this comment.
Docs should probably mention this is only intended to be used in the non-initiator contribution case
| is_splice, | ||
| target_feerate, | ||
| ); | ||
| if budget < fair_fee { |
There was a problem hiding this comment.
If we have a mixed splice where we fully consume an input and cover the remaining outstanding amount with the channel balance, then budget should probably also reflect the channel balance available to spend?
|
|
||
| let mut pending_splice: Option<PendingFunding> = None; | ||
| let mut quiescent_action = None; | ||
| let mut _quiescent_action: Option<QuiescentAction> = None; |
There was a problem hiding this comment.
You can just remove this entirely and not even mention it as its an odd TLV.
| .ok_or("Budget plus change value overflow".to_string())?; | ||
|
|
||
| match available.checked_sub(fair_fee) { | ||
| Some(new_change_value) if new_change_value >= dust_limit => { |
There was a problem hiding this comment.
In both the no-input and no-change case we fail if we'd pay a fee higher than our initially-estimated fee, but here we'll happily continue even if we'd end up reducing change or even removing it. ISTM we should maybe fail instead, as we'd maybe prefer to do our own splice with our own feerate on top of the counterparty's splice later or even splice on another channel instead instead of paying more in fees. Its complicated though, for someone with only a single channel who wants to pay immediately they'd prefer it to work, but in that setup there's a chance that the counterparty just beats them to the punch anyway, so maybe its "something they have to deal with either way" and its fine?
When both nodes want to splice simultaneously, the quiescence tie-breaker designates one as the initiator. Previously, the losing node responded with zero contribution, requiring a second full splice session after the first splice locked. This is wasteful, especially for often-offline nodes that may connect and immediately want to splice.
Instead, the losing node contributes to the winner's splice as the acceptor, merging both contributions into a single splice transaction. Since the
FundingContributionwas originally built with initiator fees (which include common fields and shared input/output weight), the fee is adjusted to the acceptor rate before contributing, with the surplusreturned to the change output.